<template>
  <el-card>
    <div slot="header">
      <span>{{ title }}</span>
      <div style="float: right">
        <el-button v-if="status !=1" type="primary" size="mini" @click="checkFlow">检查流程</el-button>
        <el-button v-if="status !=1" type="success" size="mini" @click="enableFlow">启用流程</el-button>
        <el-button v-if="status ==1" type="danger" size="mini" @click="stopFlow">停用流程</el-button>
      </div>
    </div>
    <div id="container" class="container" />
    <beginNode :readonly="status == 1" :node-id="nodeId" :def-form-id="defFormId" :flow-id="flowId" :visible.sync="visible" />
    <nodeEdit :readonly="status == 1" :flow-id="flowId" :def-form-id="defFormId" :node-id="nodeId" :nodes="nodeList" :visible.sync="nodeVisible" @close="refresh" />
    <whileNodeEdit :readonly="status == 1" :flow-id="flowId" :def-form-id="defFormId" :node-id="nodeId" :nodes="nodeList" :visible.sync="whileVisible" @close="refresh" />
    <subFlowEdit :readonly="status == 1" :flow-id="flowId" :def-form-id="defFormId" :node-id="nodeId" :nodes="nodeList" :visible.sync="subFlowVisible" @close="refresh" />
  </el-card>
</template>

<script>
import { Graph } from '@antv/x6'
import { Scroller } from '@antv/x6-plugin-scroller'
import { register } from '@antv/x6-vue-shape'
import { Snapline } from '@antv/x6-plugin-snapline'
import { FlowNodeType } from '@/flow/config/flowConfig'
import flowNode from './components/flowNodeView.vue'
import { Get, Check, Enable, Disable } from '@/flow/api/flow'
import * as flowNodeApi from '@/flow/api/flowNode'
import * as routeNodeApi from '@/flow/api/flowRouteNode'
import beginNode from './components/beginNodeEdit.vue'
import nodeEdit from './components/nodeEdit.vue'
import whileNodeEdit from './components/whileNodeEdit.vue'
import subFlowEdit from './components/subFlowNodeEdit.vue'
export default {
  components: {
    // eslint-disable-next-line vue/no-unused-components
    Graph,
    // eslint-disable-next-line vue/no-unused-components
    flowNode,
    beginNode,
    nodeEdit,
    whileNodeEdit,
    subFlowEdit
  },
  data() {
    return {
      flowId: null,
      graph: null,
      nodeId: null,
      title: '',
      visible: false,
      status: 0,
      defFormId: null,
      nodeVisible: false,
      whileVisible: false,
      subFlowVisible: false,
      errorId: null,
      width: null,
      height: null,
      nodes: {},
      nodeList: [],
      curRouteId: null,
      dropNodeId: null
    }
  },
  computed: {
    theme() {
      return this.$store.getters.theme
    }
  },
  mounted() {
    this.flowId = this.$route.params.id
    this.initFlow()
  },
  methods: {
    async enableFlow() {
      const res = await Enable(this.flowId)
      if (res.IsError === false) {
        this.$message({
          type: 'success',
          message: '启用成功'
        })
        this.status = 1
        this.init()
      } else {
        this.setNodeSate(res.Errors)
      }
    },
    async stopFlow() {
      await Disable(this.flowId)
      this.$message({
        type: 'success',
        message: '停用成功'
      })
      this.status = 2
      this.init()
    },
    setNodeSate(errors) {
      this.resetNode()
      const error = this.initNodeError(errors)
      if (error != null) {
        this.$notify({
          title: '检查结果',
          dangerouslyUseHTMLString: true,
          message: error.join('</br>'),
          duration: 0
        })
      }
    },
    async checkFlow() {
      const res = await Check(this.flowId)
      if (res.IsError === false) {
        this.$message({
          type: 'success',
          message: '检查无错误!'
        })
      } else {
        this.setNodeSate(res.Errors)
      }
    },
    resetNode() {
      if (this.errorId != null && this.errorId.length > 0) {
        this.errorId.forEach(c => {
          const id = this.nodes[c]
          if (id == null) {
            return
          }
          const cell = this.graph.getCellById(id)
          if (cell && cell.data.isError) {
            const data = Object.assign({}, cell.data)
            data.isError = false
            data.error = null
            cell.data = data
          }
        })
        this.errorId = null
      }
    },
    initNodeError(errors) {
      let error = ''
      const nodeId = []
      errors.forEach(c => {
        if (c.Type === 'node') {
          nodeId.push(c.Id)
          const cell = this.graph.getCellById(this.nodes[c.Id])
          const data = Object.assign({}, cell.data)
          data.isError = true
          data.error = c.Error
          cell.data = data
        } else {
          error = error + ' ' + c.Error
        }
      })
      this.errorId = nodeId
      if (error === '') {
        return null
      }
      return error.substring(1).split(' ')
    },
    async initFlow() {
      const res = await Get(this.flowId)
      this.defFormId = res.DefFormId
      this.status = res.Status
      this.title = '编辑流程(' + res.FlowName + ')审批流程'
      this.$nextTick(this.init)
    },
    initControl() {
      Graph.registerNode(
        'beginNode',
        {
          inherit: 'circle',
          width: 80,
          height: 80,
          attrs: {
            body: {
              strokeWidth: 3,
              stroke: '#5F95FF'
            },
            label: {
              text: '开始',
              fontSize: 16,
              fill: '#5F95FF'
            }
          }
        },
        true
      )
      Graph.registerNode(
        'subFlow',
        {
          inherit: 'ellipse',
          width: 120,
          height: 100,
          attrs: {
            body: {
              strokeWidth: 3,
              stroke: '#43AF2B'
            },
            label: {
              text: '开始',
              fontSize: 16,
              fill: '#43AF2B'
            }
          }
        },
        true
      )
      Graph.registerNode(
        'endNode',
        {
          inherit: 'circle',
          width: 80,
          height: 80,
          attrs: {
            body: {
              strokeWidth: 2,
              stroke: '#ff4949'
            },
            label: {
              text: '结束',
              fontSize: 16,
              fill: '#ff4949'
            }
          }
        },
        true
      )
      register(
        {
          shape: 'flowNode',
          width: 300,
          height: 130,
          component: flowNode
        }
      )
    },
    refresh(e) {
      if (!e.isUpdate && !e.showIsUpdate) {
        return
      }
      const node = e.node
      const cell = this.graph.getCellById(this.nodes[this.nodeId])
      if (node.NodeType === 4) {
        cell.setAttrs({
          label: {
            text: node.NodeName
          }
        })
      } else {
        const data = Object.assign({}, cell.data)
        if (e.isUpdate) {
          data.name = node.NodeName
          data.nodeType = node.NodeType
          data.auditType = node.AuditType
        }
        if (e.showIsUpdate) {
          data.show = e.show
        }
        cell.data = data
      }
      const t = this.nodeList.find(a => a.Id === this.nodeId)
      t.NodeName = node.NodeName
    },
    async addFlowNode(e) {
      const add = {
        FlowId: this.flowId,
        NodeName: '审批节点',
        Coordinate: {
          X: e.offsetX - 150,
          Y: e.offsetY
        },
        NodeType: 1
      }
      add.Id = await flowNodeApi.Add(add)
      this.addNode(add)
    },
    async addEndNode(e) {
      const add = {
        FlowId: this.flowId,
        NodeName: '审批结束节点',
        Coordinate: {
          X: e.offsetX - 40,
          Y: e.offsetY
        },
        NodeType: 3
      }
      add.Id = await flowNodeApi.Add(add)
      this.addNode(add)
    },
    async addWhileNode(e) {
      const add = {
        FlowId: this.flowId,
        NodeName: '审批循环节点',
        Coordinate: {
          X: e.offsetX - 150,
          Y: e.offsetY
        },
        NodeType: 2
      }
      add.Id = await flowNodeApi.Add(add)
      this.addNode(add)
    },
    async addSubFlowNode(e) {
      const add = {
        FlowId: this.flowId,
        NodeName: '子流程',
        Coordinate: {
          X: e.offsetX - 150,
          Y: e.offsetY
        },
        NodeType: 4
      }
      add.Id = await flowNodeApi.Add(add)
      this.addNode(add)
    },
    initMenu() {
      if (this.status === 1) {
        this.graph.off('blank:contextmenu')
        return
      }
      const that = this
      this.graph.on('blank:contextmenu', (e) => {
        const evt = e.e
        // 阻止默认的上下文菜单弹出
        evt.preventDefault()

        // 创建上下文菜单元素，这里可以根据需要自定义菜单的HTML结构
        const menuElement = document.createElement('ul')
        menuElement.style = 'position: absolute;left:' + evt.clientX + 'px;top:' + evt.clientY + 'px;transform-origin:center top;z-index: 2027;'

        menuElement.className = 'el-dropdown-menu el-popper'

        const removeMenuList = () => {
          if (menuElement.parentNode === document.body) {
            document.body.removeChild(menuElement)
          }
        }
        // 创建菜单项
        const menuItem = document.createElement('li')
        menuItem.tabindex = '-1'
        menuItem.className = 'el-dropdown-menu__item'
        const addNode = menuItem.cloneNode()
        addNode.textContent = '添加节点'
        addNode.dataset.value = 'node'
        addNode.onclick = () => {
          that.addFlowNode(evt)
          removeMenuList()
        }
        const endNode = menuItem.cloneNode()
        endNode.textContent = '添加结束节点'
        endNode.dataset.value = 'end'
        endNode.onclick = () => {
          that.addEndNode(evt)
          removeMenuList()
        }
        const whileNode = menuItem.cloneNode()
        whileNode.textContent = '添加循环节点'
        whileNode.dataset.value = 'while'
        whileNode.onclick = () => {
          that.addWhileNode(evt)
          removeMenuList()
        }
        const subNode = menuItem.cloneNode()
        subNode.textContent = '添加子流程'
        subNode.dataset.value = 'subFlow'
        subNode.onclick = () => {
          that.addSubFlowNode(evt)
          removeMenuList()
        }
        // 创建弹窗交互项
        const menuFocusItem = document.createElement('input')
        menuFocusItem.style = 'position:absolute;opacity: 0;pointer-events: none;'

        // 将菜单项添加到菜单元素中
        menuElement.append(addNode)
        menuElement.append(whileNode)
        menuElement.append(subNode)
        menuElement.append(endNode)

        // 将交互项添加到菜单元素中
        menuElement.appendChild(menuFocusItem)
        // 将菜单元素添加到DOM中
        document.body.appendChild(menuElement)
        // 鼠标离开菜单时移除菜单元素
        menuFocusItem.onblur = () => {
          setTimeout(() => {
            removeMenuList()
          }, 300)
        }
        menuFocusItem.focus()
      })
    },
    validateMagne(e) {
      return e.magnet.getAttribute('port-group') === 'bottom'
    },
    validateConnection(e) {
      if (e.sourceCell === e.targetCell) {
        return false
      } else if (e.sourceMagnet == null || e.sourceMagnet.getAttribute('port-group') !== 'bottom') {
        return false
      } else if (e.targetMagnet == null || e.targetMagnet.getAttribute('port-group') !== 'top') {
        return false
      } else if (
        e.sourceCell.data.NodeType === FlowNodeType.begin &&
        e.targetCell.data.NodeType === FlowNodeType.end
      ) {
        return false
      }
      return true
    },
    async setCoordinate(id, x, y) {
      await flowNodeApi.SetCoordinate(id, {
        X: x,
        Y: y
      })
    },
    getFlowNode(sour) {
      const node = {
        shape: 'beginNode',
        data: {
          id: sour.Id,
          name: sour.NodeName,
          nodeType: sour.NodeType,
          auditType: sour.AuditType,
          show: sour.Show
        },
        ports: {
          groups: {},
          items: []
        }
      }
      if (sour.NodeType === FlowNodeType.begin.value) {
        node.shape = 'beginNode'
        if (sour.Coordinate == null) {
          node.x = (this.width - 80) / 2
          node.y = 100
          this.setCoordinate(sour.Id, node.x, node.y)
        } else {
          node.x = sour.Coordinate.X
          node.y = sour.Coordinate.Y
        }
      } else if (sour.NodeType === FlowNodeType.end.value) {
        node.shape = 'endNode'
        node.x = sour.Coordinate.X
        node.y = sour.Coordinate.Y
      } else if (sour.NodeType === FlowNodeType.subFlow.value) {
        node.shape = 'subFlow'
        node.x = sour.Coordinate.X
        node.y = sour.Coordinate.Y
        node.attrs = {
          label: {
            text: sour.NodeName
          }
        }
      } else {
        node.shape = 'flowNode'
        node.x = sour.Coordinate.X
        node.y = sour.Coordinate.Y
      }
      if (sour.NodeType !== FlowNodeType.begin.value) {
        if (this.status !== 1) {
          node.tools = [
            {
              name: 'button-remove',
              args: {
                x: '100%',
                y: 0,
                offset: { x: 0, y: 0 }
              }
            }
          ]
        }
        node.ports.groups.top = {
          position: 'top',
          attrs: {
            circle: {
              magnet: true,
              stroke: '#8f8f8f',
              r: 5
            }
          }
        }
        node.ports.items.push({
          group: 'top',
          id: 'to'
        })
      }
      if (sour.NodeType !== FlowNodeType.end.value) {
        node.ports.groups.bottom = {
          position: 'bottom',
          attrs: {
            circle: {
              magnet: true,
              stroke: '#8f8f8f',
              r: 5
            }
          }
        }
        node.ports.items.push({
          group: 'bottom',
          id: 'from'
        })
      }
      return node
    },
    addNode(sour) {
      this.nodeList.push({
        Id: sour.Id,
        NodeName: sour.NodeName,
        NodeType: sour.NodeType
      })
      const node = this.getFlowNode(sour)
      const flowNode = this.graph.createNode(node)
      if (sour.NodeType !== FlowNodeType.begin.value) {
        const that = this
        flowNode.on('removed', e => {
          that.dropNode(e.cell)
        })
      }
      const id = this.graph.addNode(flowNode)
      this.nodes[sour.Id] = id
    },
    initLink(routes) {
      if (routes != null && routes.length > 0) {
        routes.forEach(c => {
          if (c.ToNode == null || c.ToNode.length === 0) {
            return
          }
          const sour = {
            cell: this.nodes[c.Id],
            port: 'from'
          }
          c.ToNode.forEach(n => {
            const item = {
              source: sour,
              target: {
                cell: this.nodes[n.NodeId],
                port: 'to'
              },
              data: {
                routeId: n.Id,
                toId: this.nodes[n.NodeId],
                fromId: sour.cell
              },
              tools: [],
              attrs: {
                line: {
                  stroke: '#8f8f8f',
                  strokeWidth: 1
                }
              }
            }
            if (this.status !== 1) {
              item.tools.push({
                name: 'button-remove',
                args: {
                  x: '100%',
                  y: 0,
                  offset: { x: 0, y: 0 }
                }
              })
            }
            this.graph.addEdge(item)
          })
        })
      }
    },
    async dropNode(cell) {
      this.dropNodeId = cell.id
      await flowNodeApi.Delete(cell.data.id)
      delete this.nodes[cell.id]
      this.nodeList = this.nodeList.filter(c => c.Id !== this.dropNodeId)
    },
    async dropRoute(edge) {
      if (edge.data == null || edge.data.routeId == null) {
        return
      } else if (edge.data.toId === this.dropNodeId || edge.data.fromId === this.dropNodeId) {
        return
      }
      await routeNodeApi.Delete(edge.data.routeId)
    },
    async initNode() {
      const nodes = await flowNodeApi.Gets(this.flowId)
      if (nodes.length > 0) {
        nodes.forEach(e => {
          this.addNode(e)
        })
        this.initLink(nodes)
        this.nodeList = nodes.map(c => {
          return {
            Id: c.Id,
            NodeName: c.NodeName,
            NodeType: c.NodeType
          }
        })
      } else {
        this.nodeList = []
      }
    },
    showNode(node) {
      this.nodeId = node.id
      if (node.nodeType === 0) {
        this.visible = true
      } else if (node.nodeType === 1) {
        this.nodeVisible = true
      } else if (node.nodeType === 2) {
        this.whileVisible = true
      } else if (node.nodeType === 4) {
        this.subFlowVisible = true
      }
    },
    initEvent() {
      const that = this
      this.graph.on('node:moved', e => {
        that.savePosition(e.node, e.x, e.y)
      })
      this.graph.on('edge:removed', e => {
        that.dropRoute(e.edge)
      })
      this.graph.on('node:click', e => {
        if (e.cell.data != null && e.cell.data.id != null) {
          that.showNode(e.cell.data)
        }
      })
      this.graph.on('edge:connected', ({ isNew, edge }) => {
        if (isNew) {
          edge.addTools({
            name: 'button-remove',
            args: {
              x: '100%',
              y: 0,
              offset: { x: 0, y: 0 }
            }
          })
          that.addRoute(edge)
        }
      })
    },
    async addRoute(edge) {
      if (this.curRouteId === edge.id) {
        return
      }
      this.curRouteId = edge.id
      const from = this.graph.getCellById(edge.source.cell)
      const to = this.graph.getCellById(edge.target.cell)
      if (from.data.id == null || to.data.id == null) {
        return
      }
      const id = await routeNodeApi.Add({
        FlowId: this.flowId,
        NodeId: from.data.id,
        ToNodeId: to.data.id
      })
      // eslint-disable-next-line require-atomic-updates
      edge.data = {
        routeId: id,
        toId: to.id,
        fromId: from.id
      }
    },
    savePosition(node, x, y) {
      if (node.data.id == null) {
        return
      }
      const position = node.position()
      if (position.x === x && position.y === y) {
        return
      }
      this.setCoordinate(node.data.id, position.x, position.y)
    },
    init() {
      if (this.graph == null) {
        this.initControl()
      } else {
        this.graph.dispose()
      }
      this.errorId = null
      this.nodes = {}
      this.nodeList = []
      const control = document.getElementById('container')
      let width = 1200
      let height = 600
      if (control.offsetWidth > 1200) {
        width = control.offsetWidth
      }
      if (control.offsetHeight > 600) {
        height = control.offsetHeight
      }
      this.width = width
      this.height = height
      const graph = new Graph({
        container: control,
        width: width,
        height: height,
        resizing: true,
        panning: false,
        background: {
          color: this.theme === 'dark' ? '#1d1e1f' : '#F2F7FA'
        },
        mousewheel: {
          enabled: true,
          modifiers: ['ctrl', 'meta'],
          minScale: 0.5,
          maxScale: 2
        },
        interacting: {
          nodeMovable: true,
          edgeMovable: false,
          magnetConnectable: true
        },
        connecting: {
          snap: true,
          allowBlank: false,
          allowLoop: false,
          allowPort: true,
          allowNode: false,
          allowEdge: false,
          allowMulti: false,
          highlight: false,
          validateMagnet: this.validateMagne,
          validateConnection: this.validateConnection
        },
        highlighting: {
          magnetAvailable: {
            name: 'stroke',
            args: {
              attrs: {
                fill: '#fff',
                stroke: '#A4DEB1',
                strokeWidth: 4
              }
            }
          },
          magnetAdsorbed: {
            name: 'stroke',
            args: {
              attrs: {
                fill: '#fff',
                stroke: '#31d0c6',
                strokeWidth: 4
              }
            }
          }
        }
      })
      graph.use(
        new Snapline({
          enabled: true
        })
      )
      graph.use(
        new Scroller({
          enabled: true
        })
      )
      this.graph = graph
      this.initEvent()
      this.initMenu()
      this.initNode()
    }
  }
}
</script>
<style scoped>
.container {
  width: 100%;
  min-height: calc(100vh - 210px);
}
</style>
